package com.flex.util {

   import flash.utils.Dictionary;

   import mx.binding.utils.ChangeWatcher;
   import mx.collections.ArrayCollection;
   import mx.collections.IList;
   import mx.core.IUID;
   import mx.utils.ObjectUtil;
   import mx.utils.URLUtil;
   import mx.logging.ILogger;

   /**
    * A utility class with methods dealing with various types and performing various
    * functions. A typical function is conversion of some sort, e.g., conversion between
    * types.
    */
   public class MixedUtil {
      /**
       * If this option is specified, two objects are considered equivalent if they both
       * implement IUID interface and their uid property values are the same.
       */
      public static const IUID_EQUIVALENCE:uint = 1 << 0;

      /**
       * If this option is specified, two objects are considered equivalent if property-
       * wize equivalence succeeds via <code>MixedUtil.equalObjects()</code>.
       */
      public static const PROPERTY_EQUIVALENCE:uint = 1 << 1;

      //---------------------------------------------------------------------------
      // Class Methods

      /**
       * Returns the first word in the text as delimited by the given delim.
       */
      public static function getFirstWord(text:String, delim:String):String {
         var index:int = text.indexOf(delim);

         if (index < 0) {
            return text;
         }
         var word:String = text.substring(0, index);
         return word;
      }

      /**
       * Returns the first words in the given array of strings as delimited by the given
       * delim.
       */
      public static function getFirstWords(strings:Array /*String*/, delim:String):Array /*String*/ {
         var words:Array /*String*/ = new Array(strings.length);
         var index:int = 0;

         for each (var string:String in strings) {
            var word:String = getFirstWord(string, delim);
            words[index++] = word;
         }
         return words;
      }

      /**
       * Returns substrings that are enclosed within braces, e.g., ["foo", "bar"] for
       * "x{foo}y{bar}z".
       *
       * @param count
       *    The max number of tokens to return. All tokens are returned if -1 is specified.
       */
      public static function getEvaluatableTokens(expr:String, count:int = -1):Array /*String*/ {
         var tokens:Array /*String*/ = [];
         var tokenStartIndex:int = expr.indexOf("{");

         while (tokenStartIndex >= 0 && (count < 0 || tokens.length < count)) {
            var tokenEndIndex:int = expr.indexOf("}", tokenStartIndex);

            if (tokenEndIndex > 0) {
               var token:String = expr.substring(tokenStartIndex + 1, tokenEndIndex);
               tokens.push(token);
               tokenStartIndex = expr.indexOf("{", tokenEndIndex);
            } else {
               break;
            }
         }
         return tokens;
      }

      /**
       * A specialized version of <code>getEvaluatableTokens</code> that returns at most one
       * token.
       */
      public static function getSingleEvaluatableToken(expr:String):String {
         var tokens:Array /*String*/ = getEvaluatableTokens(expr, 1);
         if (tokens.length == 0) {
            return null;
         }
         return tokens[0];
      }

      /**
       * Returns a filtered version of the given array using the given filter function and
       * an argument for the filter, if any. An item is included if the filter returns true
       * for it. The filter function can take the following forms:
       * <p>   function filter( item:T1 ):Boolean;
       * <p>   function filter( item:T1, filterArg:T2 ):Boolean;
       */
      public static function filter(array:Array, filter:Function, filterArg:* = null):Array {
         var filterFunc:Function;
         if (filterArg != null) {
            filterFunc = function(item:*, index:int, array:Array):Boolean {
               return filter(item, filterArg);
            }
         } else {
            filterFunc = function(item:*, index:int, array:Array):Boolean {
               return filter(item);
            }
         }
         var filteredArray:Array = array.filter(filterFunc);
         return filteredArray;
      }

      /**
       * Returns a filtered version of the given array using the given filter function and
       * an argument for the filter, if any. An item is excluded if the filter returns true
       * for it. The filter function can take the following forms:
       * <p>   function filter( item:T1 ):Boolean;
       * <p>   function filter( item:T1, filterArg:T2 ):Boolean;
       */
      public static function filterOut(array:Array, filter:Function,
                                       filterArg:* = null):Array {
         var filterFunc:Function;
         if (filterArg != null) {
            filterFunc = function(item:*, index:int, array:Array):Boolean {
               return !filter(item, filterArg);
            }
         } else {
            filterFunc = function(item:*, index:int, array:Array):Boolean {
               return !filter(item);
            }
         }
         var filteredArray:Array = array.filter(filterFunc);
         return filteredArray;
      }

      /**
       * Append array items to another existing array, thus avoid creating the
       * unncessary (sometimes) copy that <code>Array.concat</code> method does.
       *
       * @param toArray
       *    The array where to append the items
       *
       * @param fromArray
       *    The array from where to take the items
       */
      public static function append(toArray:Array, fromArray:Array):void {
         if (fromArray == null || fromArray.length == 0) {
            return;
         }

         if (toArray == null) {
            return;
         }

         for each (var item:* in fromArray) {
            toArray.push(item);
         }
      }

      /**
       * Returns true if the given object (1st arg) either itself is or it contains the
       * given value (2nd arg).
       */
      public static function isOrContains(object:*, value:*):Boolean {
         if (object == value) {
            return true;
         }
         if (!isIndexableOrderedCollection(object)) {
            return false;
         }
         for each (var item:* in object) {
            if (item == value) {
               return true;
            }
         }
         return false;
      }

      /**
       * Converts the given object into an Array if it's a well-known collection type, or
       * else wraps the object in a 1-item array, and returns that array.
       *
       * <p>Returns an empty array for a null input arg.
       */
      public static function objectAsArray(object:*):Array {
         if (object == null) {
            return [];
         }
         if (object is Array) {
            return object;
         }
         if (object is ArrayCollection) {
            return ArrayCollection(object).source;
         }
         if (object is IList) {
            return IList(object).toArray();
         }
         return [object];
      }

      /**
       * Returns the object itself, if it's a well known enumerable collection, or else wraps
       * the object in a 1-item Array, and returns that array.
       *
       * <p>Returns an empty array for a null input arg.
       */
      public static function objectAsCollection(object:*):* {
         if (object == null) {
            return [];
         }
         if (isIterableCollection(object)) {
            return object;
         }
         return [object];
      }

      /**
       * Extracts the keys out of the given dictionary and returns that as an array.
       */
      public static function getKeys(map:Dictionary):Array {
         var keys:Array = [];

         for (var key:Object in map) {
            keys.push(key);
         }
         return keys;
      }

      /**
       * Extracts the values out of the given dictionary and returns that as an array.
       */
      public static function getValues(map:Dictionary):Array {
         var values:Array = [];

         for (var key:Object in map) {
            values.push(map[key]);
         }
         return values;
      }

      /**
       * Checks whether the given dictionary is empty.
       *
       * @param map
       *    Dictionary to check. Note that the dictionary can be both: new style
       *    flash.utils.Dictionary class or just a plain old Object instance used
       *    as Dictionary.
       *
       * @return
       *    True, if the dictionary is empty; false otherwise.
       */
      public static function isDictionaryEmpty(map:Object):Boolean {
         for (var key:Object in map) {
            return false;
         }
         return true;
      }

      /**
       * Returns the number of items in the given dictionary.
       */
      public static function getDictionaryLength(map:Object):int {
         var len:int;
         for (var key:Object in map) {
            ++len;
         }
         return len;
      }

      /**
       * Copy <code>source</code> into <code>destination</code>. After the copy the
       * <code>destination</code> dictionary will contain all the keys and value the
       * <code>source</code> has.
       *
       * @param source
       *    A source dictionary whose key/value pairs to be transfered
       *
       * @param destination
       *    A destination dictionary where to put the key/value pairs from the <code>
       *    source</code> dictionary.
       *
       * @param overwriteDuplicates
       *    In case of duplicates in the key/value pairs from <code>source</code>
       *    and <code>destination</code>, should the <code>destination</code> values need
       *    to be overwritten with the ones from <code>source</code>.
       */
      public static function copyDictionary(source:Object, destination:Object,
                                            overwriteDuplicates:Boolean = true):void {
         for (var key:* in source) {
            if (!overwriteDuplicates && (destination[key] != null)) {
               continue;
            }

            destination[key] = source[key];
         }
      }

      /**
       * Validates whether the given objects are of the given types, where each object in
       * the first array is compared against the type at the same index in the second array.
       * If an element in the object array is null, it's considered valid.
       */
      public static function validateObjectTypes(objects:Array /*Object*/,
                                                 expectedTypes:Array /*Class*/):Boolean {
         if (objects.length != expectedTypes.length) {
            return false;
         }

         for (var i:int; i < objects.length; i++) {
            var object:Object = objects[i];

            if (object == null || object is expectedTypes[i]) {
               continue;
            }
            return false;
         }
         return true;
      }

      public static function validateObjectsType(objects:Array /*Object*/,
                                                 expectedType:Class):Boolean {
         // If the array is null or empty returns 'false'.
         if (objects == null || objects.length < 1) {
            return false;
         }

         // Iterates through each of the elements in the array.
         for each (var object:Object in objects) {
            // If the objects is not null and it's from the expected type then continue
            // iteration.
            if (object != null || object is expectedType) {
               continue;
            }

            return false;
         }

         return true;
      }

      public static function removeItemFromArray(item:Object, array:Array /*Object*/,
                                                 comparator:Function = null):Boolean {
         if (array == null) {
            return false;
         }
         for (var i:int = 0; i < array.length; i++) {
            var referencedItem:Object = array[i];
            var equal:Boolean = (item == referencedItem);
            if (comparator != null) {
               equal = (comparator(item, referencedItem) == 0);
            }
            if (equal) {
               // remove the element
               array.splice(i, 1);
               return true;
            }
         }
         return false;
      }

      /**
       * Removes the duplicate items in the given array.
       */
      public static function removeDuplicates(objects:Array /*?*/):void {
         if (objects == null || objects.length == 0) {
            return;
         }
         var objectSet:Set = new Set();
         var index:int = 0;
         while (index < objects.length) {
            var item:Object = objects[index];
            if (objectSet.contains(item)) {
               objects.splice(index, 1);
               continue;
            }
            objectSet.add(item);
            index++;
         }
      }

      public static function getSimpleClassName(qualifiedName:String):String {
         if (StringUtils.isNullorEmpty(qualifiedName)) {
            return qualifiedName;
         }
         var normalized:String = normalizeClassName(qualifiedName);
         var parts:Array = normalized.split(".");
         return parts[parts.length - 1];
      }

      /**
       * Replaces :: by . in the given qualified AS class name.
       */
      public static function normalizeClassName(name:String):String {
         if (StringUtils.isNullorEmpty(name)) {
            return name;
         }
         return name.replace("::", ".");
      }

      /**
       * Gets the array length. If array is null then returns 0.
       */
      public static function getArrayLength(array:Array):int {
         if (array == null) {
            return 0;
         }
         return array.length;
      }

      /**
       * Returns true if the given object is null, or empty. Empty means that the object has
       * a length property and the property's value is 0.
       */
      public static function isNullOrEmpty(object:Object):Boolean {
         return object == null || (object.hasOwnProperty("length") && object["length"] == 0);
      }

      /**
       * Finds the index of the given item or its equivalent in the given array. Equivalency
       * is determined through IUID interface.
       */
      public static function indexOfUidEquivalent(array:Array, object:Object,
                                                  fromIndex:int = 0):int {
         if (array == null || array.length == 0) {
            return -1;
         }
         var index:int = array.indexOf(object, fromIndex);
         if (index != -1) {
            return index;
         }
         for (index = fromIndex; index < array.length; index++) {
            var item:Object = array[index];
            if (areIuidEquivalent(item, object)) {
               return index;
            }
         }
         return -1;
      }

      public static function equalObjects(object1:Object, object2:Object):Boolean {
         if (object1 == object2) {
            return true;
         }
         if (object1 is IUID && object2 is IUID) {
            return IUID(object1).uid == IUID(object2).uid;
         }
         return ObjectUtil.compare(object1, object2) == 0;
      }

      public static function equalNonNullObjects(object1:Object, object2:Object):Boolean {
         if (object1 == null || object2 == null) {
            return false;
         }

         return equalObjects(object1, object2);
      }

      public static function areEquivalentCollections(collection1:Object,
                                                      collection2:Object, options:uint = 0):Boolean {
         if (!isIndexableOrderedCollection(collection1) ||
            !isIndexableOrderedCollection(collection2)) {
            return false;
         }
         if (collection1.length != collection2.length) {
            return false;
         }
         var uidEquivalenceApplies:Boolean = (options & IUID_EQUIVALENCE) ==
            IUID_EQUIVALENCE;
         var propertyEquivalenceApplies:Boolean = (options & PROPERTY_EQUIVALENCE) ==
            PROPERTY_EQUIVALENCE;
         for (var i:int; i < collection1.length; i++) {
            var object1:Object = collection1[i];
            var object2:Object = collection2[i];
            if (object1 == object2) {
               continue;
            }
            if (uidEquivalenceApplies && areIuidEquivalent(object1, object2)) {
               continue;
            }
            if (propertyEquivalenceApplies && equalObjects(object1, object2)) {
               continue;
            }
            return false;
         }
         return true;
      }

      /**
       * Indicates whether the given object is a collection that can be indexed by an
       * integer, is ordered and has a <code>length</code> property.
       */
      public static function isIndexableOrderedCollection(collection:Object):Boolean {
         return collection is Array || collection is IList;
      }

      /**
       * Returns true if the given collection can be iterated over using a "for each" loop.
       */
      public static function isIterableCollection(collection:Object):Boolean {
         return isIndexableOrderedCollection(collection) || collection is Dictionary;
      }

      /**
       * Returns true if both the arguments are IUID and their uid properties have the same
       * value.
       */
      public static function areIuidEquivalent(object1:Object, object2:Object):Boolean {
         if (!(object1 is IUID) || !(object2 is IUID)) {
            return false;
         }
         return IUID(object1).uid == IUID(object2).uid;
      }

      public static function arraysIntersect(arr1:Array /*?*/, arr2:Array /*?*/):Boolean {
         var set1:Set /*?*/ = Set.fromArray(arr1);
         for each (var elem2:* in arr2) {
            if (set1.contains(elem2)) {
               return true;
            }
         }
         return false;
      }

      public static function bindProperty(site:Object, prop:String, host:Object,
                                          chain:Object, commitOnly:Boolean = false, useWeakReference:Boolean = false,
                                          predicate:Function = null):ChangeWatcher {
         var w:ChangeWatcher = ChangeWatcher.watch(host, chain, null, commitOnly,
            useWeakReference);
         if (w == null) {
            return w;
         }
         var assign:Function = function(event:*):void {
            if (predicate == null || predicate(site, prop, host, chain)) {
               site[prop] = w.getValue();
            }
         };
         w.setHandler(assign);
         var needsAssign:Boolean;
         // Added try-catch to handle the case where the property is write only.
         try {
            needsAssign = site[prop] != w.getValue();
         } catch (error:Error) {
            needsAssign = true;
         }
         if (needsAssign) {
            assign(null);
         }
         return w;
      }

      /**
       * Checks if the error message contains the given server error type.
       * Returns true if it does, else false.
       */
      public static function isServerErrorOfType(error:Error, type:String):Boolean {
         if (error == null || StringUtils.isNullorEmpty(error.message)) {
            return false;
         }

         return error.message.indexOf(type) != -1;
      }

      public static function isLocalHost(ipOrUrl:String):Boolean {
         return ipOrUrl == "127.0.0.1" ||
            ipOrUrl == "0:0:0:0:0:0:0:1" ||
            ipOrUrl == "::1" ||
            ipOrUrl == "localhost";
      }

      public static function roundUp(value:Number, precision:int=0):Number {
         var n:Number = Math.pow(10, precision);
         return Math.ceil(value * n) / n;
      }

      public static function round(value:Number, precision:int=0):Number {
         var n:Number = Math.pow(10, precision);
         return Math.round(value * n) / n;
      }

      public static function getServerNameFromUrl(url:String):String {
         var serverName:String = URLUtil.getServerName(url);
         if (StringUtils.isNullorEmpty(serverName)) {
            return serverName;
         }

         if (serverName.charAt(serverName.length-1) == ']' && serverName.charAt(0) != '[') {
            serverName = '[' + serverName;
         }
         return serverName;
      }

      public static function logAndThrow(logger:ILogger, ex:Error):void {
         if (logger == null || ex == null) {
            return;
         }
         logger.error(ex.message);
         throw ex;
      }

      public static function compareProperty(obj1:Object, obj2:Object,
                                             propertyName:String):int {
         if (obj1 == obj2) {
            return 0;
         }
         if (obj1 == null) {
            return 1;
         }
         if (obj2 == null) {
            return -1;
         }
         if (!obj1.hasOwnProperty(propertyName) || !obj2.hasOwnProperty(propertyName)) {
            throw new Error("Property comparator invoked with a type that does not have " +
               "the property '" + propertyName + "' to compare.");
         }
         return ObjectUtil.compare(obj1[propertyName], obj2[propertyName]);
      }

   } // class
} // package